using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace csammisrun.OscarLib.Utility
{
  class FileTransferConnection : DirectConnection
  {
    private string _localfilename = "";
    private Encoding _localfilenameencoding = Marshal.ASCII;
    private FileHeader _header = new FileHeader();
    private int _totalfiles = 1;
    private int _filesremaining = 0;
    private uint _totalsize = 0;
    private int _totalparts = 0;
    private bool _cancelling = false;

    private byte[] _datachunk = null;
    private Stream _datastream = null;
    private uint _streamposition = 0;

    public FileTransferConnection(Session parent, int id, DirectConnectionMethod dcmethod, DirectConnectRole role)
      : base(parent, id, dcmethod, role)
    {
      Logging.WriteString("Creating new FTC (id " + id.ToString() + ")");
      base.DirectConnectionFailed += new DirectConnectionFailed(FileTransferConnection_DirectConnectionFailed);
      base.DirectConnectionReady += new DirectConnectionReady(FileTransferConnection_DirectConnectionReady);
    }

    #region Sending file methods
    /// <summary>
    /// Begins to send a file through the RendezvousData's Transfer socket
    /// </summary>
    /// <remarks>This method cancels the direct connection itself rather than throw an exception</remarks>
    private void BeginSendFile()
    {
      // Open the file for reading
      try
      {
        _datachunk = new byte[8192];
        _datastream = (new StreamReader(LocalFileName, false)).BaseStream;
        _streamposition = 0;
      }
      catch (Exception)
      {
        CancelFileTransfer("Can't open file for reading");
        return;
      }

      // Send a PROMPT message and receive an ACK message
      try
      {
        SendFileTransmitterHandshake();
      }
      catch (Exception ex)
      {
        CancelFileTransfer(ex.Message);
        return;
      }

      // Signal the parent session that we've started transfering a file
      _parent.OnFileTransferProgress(Cookie, 0, FileHeader.Size);

      // Send the first chunk
      SendFileTransmitterSendChunk();
    }

    /// <summary>
    /// Send an OFT PROMPT message and receive an ACK message
    /// </summary>
    private void SendFileTransmitterHandshake()
    {
      byte[] buffer = Marshal.CreateFileTransferMessage(0x0101, this);
      int index;

      // Send a PROMPT with a blank cookie
      for (index = 8; index < 16; index++)
        buffer[index] = 0x00;

      index = 0;
      try
      {
        while (index < buffer.Length)
        {
          index += _sock.Send(buffer, index, buffer.Length - index, SocketFlags.None);
        }
      }
      catch (Exception ex)
      {
        throw ex;
      }

      Logging.DumpFLAP(buffer, "Sent PROMPT");

      // Receive the return ACK
      index = 0;
      try
      {
        while (index < buffer.Length)
        {
          index += _sock.Receive(buffer, index, buffer.Length - index, SocketFlags.None);
        }
      }
      catch (Exception ex)
      {
        throw ex;
      }

      Logging.DumpFLAP(buffer, "Received ACK");

      // Compare cookies to verify everything's okay
      for (int i = 0; i < 8; i++)
      {
        if (buffer[i + 8] != Cookie[i])
        {
          throw new Exception("Recepient sent a bad cookie. Possible man-in-the-middle?");
        }
      }
    }

    /// <summary>
    /// Sends a chunk of a file to the recepient
    /// </summary>
    private void SendFileTransmitterSendChunk()
    {
      int bytesreadfromfile = 0;
      try
      {
        // Read a chunk from the file
        bytesreadfromfile = _datastream.Read(_datachunk, 0, _datachunk.Length);

        // Begin to send it over the wire
        _sock.BeginSend(_datachunk, 0, bytesreadfromfile, SocketFlags.None, new AsyncCallback(SendFileTransmitterSendEnd), null);
      }
      catch (Exception ex)
      {
        CancelFileTransfer(ex.Message);
      }
    }

    /// <summary>
    /// Called when a chunk of data has been sent to the remote client
    /// </summary>
    /// <param name="res">An <see cref="IAsyncResult"/> object</param>
    private void SendFileTransmitterSendEnd(IAsyncResult res)
    {
      int bytessent = 0;
      try
      {
        bytessent = _sock.EndSend(res);
      }
      catch (Exception ex)
      {
        CancelFileTransfer(ex.Message);
        return;
      }

      _streamposition += (uint)bytessent;

      // Signal parent session of the progress
      _parent.OnFileTransferProgress(Cookie, _streamposition, FileHeader.Size);

      // If all the file has been sent, wait for the acknowledgement message
      // and close up shop. Otherwise, send another chunk
      if (_streamposition == FileHeader.Size)
      {
        byte[] receivedone = new byte[256];
        try
        {
          _sock.Receive(receivedone);
        }
        catch (Exception ex)
        {
          CancelFileTransfer(ex.Message);
          return;
        }

        // Check checksums?

        CompleteFileTransfer();
      }
      else
      {
        SendFileTransmitterSendChunk();
      }
    }
    #endregion

    #region Receiving file methods
    /// <summary>
    /// Receives the PROMPT message and responds with an ACK message, then prepares the transfer socket to receive data
    /// </summary>
    private void BeginReceiveFile()
    {
      byte[] filetransferheader = null;
      int index = 0;

      // Read in 256 bytes, PROMPT type and blank cookie

      filetransferheader = new byte[256];
      try
      {
        while (index < filetransferheader.Length)
        {
          index += _sock.Receive(filetransferheader, index, filetransferheader.Length - index, SocketFlags.None);
        }
      }
      catch (Exception ex)
      {
        string message = "Error negotiating file transfer:"
              + Environ.NewLine + ex.Message;
        CancelFileTransfer(message);
      }

      index = 8;
      Marshal.ByteArrayToFTI(filetransferheader, ref index, this);

      // Respond with the same header, but with the ACK type and the ICBM cookie set
      index = 6;
      Marshal.InsertUshort(filetransferheader, 0x0202, ref index);
      Marshal.CopyArray(Cookie, filetransferheader, 0, ref index);

      index = 0;
      try
      {
        while (index < filetransferheader.Length)
        {
          index += _sock.Send(filetransferheader, index, filetransferheader.Length - index, SocketFlags.None);
        }
      }
      catch (Exception ex)
      {
        string message = "Error negotiating file transfer:"
			+ Environ.NewLine + ex.Message;
        CancelFileTransfer(message);
      }

      // Open the file for writing
      try
      {
        _datachunk = new byte[8192];
        _datastream = (new StreamWriter(LocalFileName, false)).BaseStream;
        _streamposition = 0;
      }
      catch (Exception)
      {
        throw new Exception("Can't open target file for writing");
      }

      // Signal the parent session that the file transfer has started
      _parent.OnFileTransferProgress(Cookie, 0, FileHeader.Size);

      // Start receiving data
      _sock.BeginReceive(_datachunk, 0, _datachunk.Length, SocketFlags.None, new AsyncCallback(SendFileTransferReceive), null);
    }

    /// <summary>
    /// The callback function for socket reads during a file transfer
    /// </summary>
    /// <param name="res">An <see cref="IAsyncResult"/> object</param>
    private void SendFileTransferReceive(IAsyncResult res)
    {
      int bytesread = 0;

      // End the async receive operation
      try
      {
        bytesread = _sock.EndReceive(res);
        if (bytesread == 0)
          throw new Exception("Remote client cancelled transfer");
      }
      catch (Exception ex)
      {
        CancelFileTransfer(ex.Message);
        return;
      }

      // Write the received data out to the file
      try
      {
        _datastream.Write(_datachunk, 0, bytesread);
        _streamposition += (uint)bytesread;
      }
      catch (Exception ex)
      {
        CancelFileTransfer(ex.Message);
        return;
      }

      // Checksum the received chunk
      FileHeader.ReceivedChecksum = ChecksumChunk(_datachunk, (uint)bytesread, FileHeader.ReceivedChecksum);

      // Signal progress to the parent session
      _parent.OnFileTransferProgress(Cookie, _streamposition, FileHeader.Size);

      // Check to see if the transfer has finished
      if (_streamposition >= FileHeader.Size)
      {
        _datachunk = null;
        _datastream.Close();

        // Send out the acknowledgement, compare the checksums, and finish up
        SendFileReceiverDone();

        if (FileHeader.ReceivedChecksum != FileHeader.Checksum)
        {
          CancelFileTransfer("Received data does not match expected checksum");
          return;
        }

        CompleteFileTransfer();
      }
      else
      {
        // Keep receiving asynchronously
        _sock.BeginReceive(_datachunk, 0, _datachunk.Length,
          SocketFlags.None, new AsyncCallback(SendFileTransferReceive), null);
      }
    }

    /// <summary>
    /// Sends a DONE message
    /// </summary>
    private void SendFileReceiverDone()
    {
      Marshal.CopyArray(Cookie, FileHeader.Cookie, 0);
      byte[] buffer = Marshal.CreateFileTransferMessage(0x0204, this);
      int index = 0;
      try
      {
        while (index < buffer.Length)
        {
          index += _sock.Send(buffer, index, buffer.Length - index, SocketFlags.None);
        }
      }
      catch (Exception ex)
      {
        CancelFileTransfer(ex.Message);
      }
    }
    #endregion

    #region Transfer shutdown methods
    void CompleteFileTransfer()
    {
      CloseTransfer("", false);
    }

    /// <summary>
    /// Cancel an in-progress file transfer
    /// </summary>
    public void CancelFileTransfer(string reason)
    {
      CloseTransfer(reason, true);
    }

    private void CloseTransfer(string message, bool error)
    {
      if (_cancelling)
        return;

      _cancelling = true;

      try
      {
        if (_listener != null && _listener.Connected)
        {
          _listener.Shutdown(System.Net.Sockets.SocketShutdown.Both);
          _listener.Close();
        }
      }
      catch (Exception) { }

      try
      {
        if (_sock != null && _sock.Connected)
        {
          _sock.Shutdown(System.Net.Sockets.SocketShutdown.Both);
          _sock.Close();
        }
      }
      catch (Exception) { }

      try
      {
        if (_datastream != null)
        {
          _datastream.Close();
        }
      }
      catch (Exception) { }

      _datachunk = null;

      if (error)
      {
        _parent.OnFileTransferCancelled(Cookie, message);
        SNAC04.SendDirectConnectionCancellation(_parent, this, message);
      }
      else
        _parent.OnFileTransferCompleted(Cookie);

      // Remove connection from ConnManager
    }
    #endregion

    #region Direct connection event handlers
    void FileTransferConnection_DirectConnectionReady()
    {
      // The transfer socket's ready to go, we just have to figure out what we're doing and do it
      if (Role == DirectConnectRole.Initiator)
      {
        BeginSendFile();
      }
      else
      {
        BeginReceiveFile();
      }
    }

    void FileTransferConnection_DirectConnectionFailed(string reason)
    {
      _parent.OnFileTransferCancelled(_cookie, reason);
    }
    #endregion

    #region Properties
    /// <summary>
    /// Gets or sets the path to the local file to send or receive
    /// </summary>
    public string LocalFileName
    {
      get { return _localfilename; }
      set
      {
        _localfilename = value;

        if (File.Exists(_localfilename))
        {
          // Set all the file-dependent data
          int slashindex = _localfilename.LastIndexOf("\\");
          _header.Name = _localfilename.Substring(slashindex + 1, _localfilename.Length - (slashindex + 1));
          _header.Size = (uint)(new FileInfo(_localfilename)).Length;
          _header.Checksum = Checksum();
          _totalfiles = 1;
          _totalparts = 1;
          _filesremaining = 1;
          _totalsize = _header.Size;
        }
      }
    }

    /// <summary>
    /// Gets or sets the encoding of the filename to send
    /// </summary>
    public Encoding LocalFileNameEncoding
    {
      get { return _localfilenameencoding; }
      set { _localfilenameencoding = value; }
    }

    /// <summary>
    /// Returns the SendFiles capability
    /// </summary>
    public override Capabilities Capability
    {
      get
      {
        return Capabilities.SendFiles;
      }
    }

    public FileHeader FileHeader
    {
      get { return _header; }
    }

    /// <summary>
    /// Gets the total number of files to send
    /// </summary>
    public int TotalFiles
    {
      get { return _totalfiles; }
      set { _totalfiles = value; }
    }

    public int FilesRemaining
    {
      get { return _filesremaining; }
      set { _filesremaining = value; }
    }

    public int TotalParts
    {
      get { return _totalparts; }
      set { _totalparts = value; }
    }

    /// <summary>
    /// Gets the total number of bytes to send
    /// </summary>
    public uint TotalFileSize
    {
      get { return _totalsize; }
      set { _totalsize = value; }
    }
    #endregion

    /// <summary>
    /// Performs an OSCAR File Transfer checksum 
    /// </summary>
    /// <param name="sess">A <see cref="Session"/> object</param>
    /// <param name="filename">The file to checksum</param>
    /// <returns>The checksum of the file</returns>
    private uint Checksum()
    {
      uint retval = 0xFFFF0000;
      FileStream infile = null;

      try
      {
        infile = File.OpenRead(LocalFileName);
        byte[] chunk = new byte[1024];
        int bytesread = 0;
        int streamposition = 0;
        do
        {
          bytesread = infile.Read(chunk, 0, chunk.Length);
          retval = ChecksumChunk(chunk, (uint)bytesread, retval);
          streamposition += bytesread;

        } while (bytesread == chunk.Length);
      }
      catch (Exception ex)
      {
        CancelFileTransfer(ex.Message);
        return 0xFFFFFFFF;
      }
      finally
      {
        infile.Close();
      }
      return retval;
    }

    public static uint ChecksumChunk(byte[] filechunk, uint chunklength, uint start)
    {
      uint retval = (start >> 16) & 0xFFFF;
      uint oldretval;
      ushort filebyte;

      for (int i = 0; i < filechunk.Length && i < chunklength; i++)
      {
        oldretval = retval;
        filebyte = filechunk[i];
        if ((i & 1) == 0)
        {
          filebyte <<= 8;
        }
        retval -= filebyte;
        if (retval > oldretval)
          retval--;
      }
      retval = ((retval & 0x0000ffff) + (retval >> 16));
      retval = ((retval & 0x0000ffff) + (retval >> 16));
      return retval << 16;
    }
  }
}
